using System;
using System.IO;
using System.Collections.Specialized;
using System.Web;
using System.Web.SessionState;
using System.Net;
using System.Text;
using System.Security;
using System.Threading;
using System.Configuration;

namespace GetExceptional
{
  internal class NotFoundException : Exception { }

  public class Exceptional
  {
    public static void ReportNotFound(HttpRequest req) { ReportNotFound(req, null); }
    public static void ReportNotFound(HttpRequest req, HttpSessionState sess)
    {
      ReportException(new GetExceptional.NotFoundException(), req, sess);
    }

    public static void ReportException(Exception ex, HttpRequest req) { ReportException(ex, req, null); }
    public static void ReportException(Exception ex, HttpRequest req, HttpSessionState sess)
    {
      ReportException(ex, req, sess, true);
    }

    /**
     * Send true to fire the request in a thread, false to do it synchronously
     */
    public static void ReportException(Exception ex, HttpRequest req, HttpSessionState sess, bool async)
    {
      ExceptionalSender sender = new ExceptionalSender(new ExceptionalData(ex, req, sess));
      if (async)
      {
        Thread senderThread = new Thread(new ThreadStart(sender.SendException));
        senderThread.Start();
      }
      else
        sender.SendException();
    }
  }

  /// <summary>
  /// This is the class responsible for sending the data.  It is structured in a paramaterless
  /// way so it can be executed in a thread.
  /// </summary>
  public class ExceptionalSender
  {
    private static readonly string ProtocolVersion = "2";
    private static string ApiKey;
    private static string UrlEndPoint = "http://plugin.getexceptional.com/api/errors";
    private static string PropertyNameApiKey = "GetExceptionalApiKey";

    private ExceptionalData m_Data;

    static ExceptionalSender()
    {
      ApiKey = ConfigurationSettings.AppSettings[PropertyNameApiKey];
      if (null == ApiKey)
      {
        throw new Exception("The ApiKey for GetExceptional was not provided.  Did you include " + PropertyNameApiKey + " in the appSettings section of your web.config?");
      }
      string overrideUrl = ConfigurationSettings.AppSettings["GetExceptionalUrl"];
      if (overrideUrl != null) UrlEndPoint = overrideUrl;
    }

    public ExceptionalSender(ExceptionalData data)
    {
      m_Data = data;
    }

    public void SendException()
    {
      byte[] bytes = Encoding.UTF8.GetBytes(m_Data.ToXml());
      WebRequest req = WebRequest.Create(ExceptionalSender.UrlEndPoint + "?api_key=" + ApiKey + "&protocol_version=" + ProtocolVersion);
      req.Method = "POST";
      req.ContentType = "text/xml";
      req.ContentLength = bytes.Length;
      using (Stream requestStream = req.GetRequestStream())
      {
        requestStream.Write(bytes, 0, bytes.Length);
      }

      HttpStatusCode code;
      try
      {
        using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
        {
          code = response.StatusCode;
        }
        if (code != HttpStatusCode.Created && code != HttpStatusCode.OK)
        {
          //we only throw exceptions in debug mode, otherwise they're swallowed for production.
#if DEBUG
          throw new Exception("Unexpected response from " + ExceptionalSender.UrlEndPoint + " - " + code);
#endif
        }
      }
      catch (Exception)
      {
        //These exceptions will happen if GetExceptional goes down altogether.  Swallow
        //in production again
#if DEBUG
        throw;
#endif
      }
    }
  }

  /// <summary>
  /// This class encapsulates the data that is to be sent, and is capale of producing 
  /// an XML representation of itself suitable for consumption by the web service.
  /// </summary>
  public class ExceptionalData
  {
    private Exception m_Exception;
    private HttpRequest m_Request;
    private HttpSessionState m_Session;
    private DateTime m_ExceptionTime = DateTime.Now.ToUniversalTime();

    public ExceptionalData(Exception ex, HttpRequest req)
    {
      m_Exception = ex;
      m_Request = req;
    }
    public ExceptionalData(Exception ex, HttpRequest req, HttpSessionState sess)
      : this(ex, req)
    {
      m_Session = sess;
    }

    public string ExceptionClass
    {
      get
      {
        if (null == m_Exception) return null;
        //A little hack-ish so we can get good 404's in GetExceptional
        if (m_Exception is GetExceptional.NotFoundException) return "ActionController::RoutingError";
        return m_Exception.GetType().ToString();
      }
    }

    /// <summary>
    /// Given an HTTP Request and an Exception, turns it into XML in a format that 
    /// getexceptional.com can consume
    /// </summary>
    /// <param name="ex">Exception you want to report</param>
    /// <param name="req">Request during which the exception was thrown.  This can be
    ///                   null, so that you cann use this in non-web environments.
    ///                   </param>
    /// <returns></returns>
    public string ToXml()
    {
      string controller = "[no controller]";
      string action = "[no action]";
      string root = "[no root]";
      string url = "[no url]";

      //create a new copy of the servervariables because sometimes they can change during the 
      //iteration of them, which causes errors
      NameValueCollection serverVariables = null;
      try
      {
        if (null != m_Request) serverVariables = new NameValueCollection(m_Request.ServerVariables);
      }
      catch (Exception)
      {
        //I have seen occasions where IIS throws exceptions accessing server variables
#if DEBUG
        throw;
#endif
      }

      if (null != m_Request)
      {
        action = m_Request.FilePath;
        if (null != serverVariables) controller = serverVariables["HTTP_HOST"];
        root = m_Request.ApplicationPath;
        url = (null != m_Request.Url) ? m_Request.Url.ToString() : "";
      }



      string exceptionMessage = "[no message]";
      string exceptionStack = "[no stack trace]";
      if (m_Exception != null)
      {
        exceptionMessage = m_Exception.Message;
        exceptionStack = m_Exception.StackTrace;
      }

      //do this with a stringbuilder instead of XML tools (ex. LINQ) for the sake of speed.
      StringBuilder sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
      sb.Append("<error>\n");
      sb.Append("\t<controller_name>");
      sb.Append(controller);
      sb.Append("</controller_name>\n");
      sb.Append("\t<action_name>");
      sb.Append(action);
      sb.Append("</action_name>\n");
      sb.Append("\t<error_class>");
      sb.Append(ExceptionClass ?? "[unknown]");
      sb.Append("</error_class>\n");
      sb.Append("\t<message>");
      sb.Append(ExceptionalData.XMLEscape(exceptionMessage));
      sb.Append("</message>\n");
      sb.Append("\t<backtrace>");
      sb.Append(ExceptionalData.XMLEscape(exceptionStack));
      sb.Append("</backtrace>\n");
      sb.Append("\t<occurred_at>");
      sb.Append(m_ExceptionTime.ToString());
      sb.Append("</occurred_at>\n");
      sb.Append("\t<rails_root>");
      sb.Append(root);
      sb.Append("</rails_root>\n");
      sb.Append("\t<url>");
      sb.Append(ExceptionalData.XMLEscape(url));
      sb.Append("</url>\n");
      sb.Append("\t<environment>\n");
      if (null != serverVariables) ExceptionalData.AppendXML(serverVariables, sb);
      sb.Append("</environment>\n");
      sb.Append("\t<session>\n");
      if (null != m_Session)
      {
        System.Collections.IEnumerator ie = m_Session.GetEnumerator();
        string currentSessionItemName = "";
        string currentSessionItemValue = "";
        try
        {
          while (ie.MoveNext())
          {
            currentSessionItemName = (string)ie.Current;
            currentSessionItemValue = (null == m_Session[currentSessionItemName]) ? "NULL" : m_Session[currentSessionItemName].ToString();
            AppendXML(currentSessionItemName, currentSessionItemValue, sb);
          }
        }
        catch (Exception ex)
        {
          AppendXML("ExceptionalException", ex.Message, sb);
        }
      }
      sb.Append("</session>\n");
      sb.Append("\t<parameters>\n");
      if (null != m_Request && null != m_Request.QueryString) ExceptionalData.AppendXML(m_Request.QueryString, sb);
      if (null != m_Request && null != m_Request.Form) ExceptionalData.AppendXML(m_Request.Form, sb);
      sb.Append("</parameters>\n");
      sb.Append("</error>");

      return sb.ToString();
    }


    //Appends a NameValueCollection to a StringBuilder as XML
    private static void AppendXML(NameValueCollection nvc, StringBuilder sb)
    {
      if (null == nvc) return;
      foreach (string key in nvc.Keys) { AppendXML(key, nvc[key], sb); }
    }

    //Turns the name into a tag, and makes the value it's inner text, and appends to the stringbuilder
    private static void AppendXML(string name, string value, StringBuilder sb)
    {
      if (name == null || value == null) return;
      sb.Append("<"); sb.Append(name); sb.Append(">");
      sb.Append(ExceptionalData.XMLEscape(value));
      sb.Append("</"); sb.Append(name); sb.Append(">\n");
    }

    private static string XMLEscape(string text)
    {
      return SecurityElement.Escape(text);
    }
  }
}